package org.xx.armory.doc.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.doc.Block;
import org.xx.armory.doc.Breaker;
import org.xx.armory.doc.Document;
import org.xx.armory.doc.DocumentFactory;
import org.xx.armory.doc.FullImg;
import org.xx.armory.doc.HyperLink;
import org.xx.armory.doc.Img;
import org.xx.armory.doc.PageBreaker;
import org.xx.armory.doc.Paragraph;
import org.xx.armory.doc.Table;
import org.xx.armory.doc.TableCell;
import org.xx.armory.doc.TableRow;
import org.xx.armory.doc.Text;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.Location;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.math.NumberUtils.toInt;
import static org.xx.armory.commons.Validators.rejectIfBlank;
import static org.xx.armory.commons.Validators.rejectIfNull;

/**
 * Html文档工厂。
 *
 * <p>如果待解析的HTML文档中没有引用DTD，那么不能使用实体名字引用，必须使用实体数字引用。</p>
 * <pre>
 *     &amp;nbsp; -&gt; &amp;#160;
 *     &amp;amp; -&gt; &amp;#38;
 *     &amp;lt; -&gt; &amp;#60;
 *     &amp;gt; -&gt; &amp;#62;
 *     &amp;copy; -&gt; &amp;#169;
 * </pre>
 * <p>更多实体引用参见 https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references</p>
 *
 * @author Haart
 */
public final class HtmlDocumentFactory
        extends DocumentFactory {
    private static final Logger LOGGER = LoggerFactory.getLogger(HtmlDocumentFactory.class);
    private static final String BODY_TAG_NAME = "body";

    /**
     * 构造HTML文档工厂实例。
     */
    public HtmlDocumentFactory() {
    }

    /**
     * 从Reader中读取块元素。
     *
     * @param reader
     *         读取XML的Reader。
     * @return 遍历所有块元素的迭代器。
     * @throws XMLStreamException
     *         如果读取XML文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     */
    private static Iterable<Block> loadBlocks(
            XMLStreamReader reader
    )
            throws XMLStreamException {
        rejectIfNull(reader, "reader");

        return () -> new BlockIterator(reader);
    }

    /**
     * 从Reader中读取段落元素，使用空字符串作为默认的元素样式类名。
     *
     * @param reader
     *         读取XML的Reader。
     * @return 读取的元素。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @see #loadParagraph(XMLStreamReader, String)
     */
    private static Paragraph loadParagraph(
            XMLStreamReader reader
    )
            throws XMLStreamException {
        return loadParagraph(reader, "");
    }

    /**
     * 从Reader中读取换页符。
     *
     * @param reader
     *         读取XML的Reader。
     * @return 读取的元素。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     */
    private static PageBreaker loadPageBreaker(
            XMLStreamReader reader
    )
            throws XMLStreamException {
        rejectIfNull(reader, "reader");

        final var className = loadClassName(reader);
        final var landscape = asList(className.split("\\s+")).contains("landscape");

        final var pb = new PageBreaker(landscape);

        LOGGER.trace("[PageBreaker] at {}", location(reader));

        return pb;
    }

    /**
     * 从Reader中读取表格元素。
     *
     * @param reader
     *         读取XML的Reader。
     * @return 读取的元素。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @see #loadTable(XMLStreamReader, String)
     */
    private static Table loadTable(XMLStreamReader reader)
            throws XMLStreamException {
        rejectIfNull(reader, "reader");

        return loadTable(reader, "");
    }

    /**
     * 从Reader中读取段落元素，如果该元素没有指定样式类名则使用默认的样式类名。
     *
     * @param reader
     *         读取XML的Reader。
     * @param defaultClassName
     *         默认的样式类名。
     * @return 读取的元素。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @see #loadParagraph(XMLStreamReader)
     */
    private static Paragraph loadParagraph(
            XMLStreamReader reader,
            String defaultClassName
    )
            throws XMLStreamException {
        rejectIfNull(reader, "reader");

        final var tag = reader.getLocalName();
        final var className = loadClassName(reader);
        final var p = new Paragraph(isBlank(className) ? defaultClassName : className);

        LOGGER.trace("[P] at {}", location(reader));

        boolean strong = false;
        String localName;
        while (reader.hasNext()) {
            reader.next();
            if (reader.isWhiteSpace()) {
                // 跳过空白字符。
            } else if (reader.isCharacters()) {
                final Text plainText = loadText(reader, strong);
                if (!plainText.isEmpty()) {
                    p.add(plainText);
                }
            } else if (reader.isStartElement()) {
                localName = reader.getLocalName();
                if ("strong".equalsIgnoreCase(localName)) {
                    strong = true;
                } else if ("br".equalsIgnoreCase(localName)) {
                    p.add(new Breaker());
                } else if ("span".equals(localName)) {
                    p.add(loadSpan(reader, strong));
                } else if ("a".equals(localName)) {
                    p.add(loadHyperLink(reader, strong));
                } else if ("img".equals(localName)) {
                    p.add(loadImg(reader, "[IMG]"));
                } else {
                    LOGGER.warn("invalid inline element: <{}> at {}", localName, location(reader));
                    skipFullElement(reader);
                }
            } else if (reader.isEndElement()) {
                localName = reader.getLocalName();
                if ("strong".equalsIgnoreCase(localName)) {
                    strong = false;
                } else if (localName.equals(tag)) {
                    break;
                }
            }
        }

        return p;
    }

    /**
     * 从Reader中读取表格元素，如果该元素没有指定样式类名则使用默认的样式类名。
     *
     * @param reader
     *         读取XML的Reader。
     * @param defaultClassName
     *         默认的样式类名。
     * @return 读取的元素。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @see #loadTable(XMLStreamReader)
     */
    private static Table loadTable(
            XMLStreamReader reader,
            String defaultClassName
    )
            throws XMLStreamException {
        rejectIfNull(reader, "reader");

        final String className = loadClassName(reader);
        final String tagName = reader.getLocalName();

        final Table t = new Table(isBlank(className) ? defaultClassName : className);

        LOGGER.trace("[TABLE] at {}", location(reader));

        String localName;
        while (reader.hasNext()) {
            reader.next();

            if (reader.isStartElement()) {
                localName = reader.getLocalName();
                if ("tr".equalsIgnoreCase(localName)) {
                    t.add(loadTableRow(reader, ""));
                } else {
                    skipFullElement(reader);
                }
            } else if (reader.isEndElement() && tagName.equalsIgnoreCase(reader.getLocalName())) {
                break;
            }
        }

        return t;
    }

    /**
     * 从Reader中读取表格行元素，如果该元素没有指定样式类名则使用默认的样式类名。
     *
     * @param reader
     *         读取XML的Reader。
     * @param defaultClassName
     *         默认的样式类名。
     * @return 读取的元素。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @see #loadTable(XMLStreamReader, String)
     */
    private static TableRow loadTableRow(
            XMLStreamReader reader,
            String defaultClassName
    )
            throws XMLStreamException {
        rejectIfNull(reader, "reader");

        final String className = loadClassName(reader);
        final String tagName = reader.getLocalName();

        final TableRow r = new TableRow(className.length() == 0 ? defaultClassName : className);

        LOGGER.trace("[TABLE ROW] at {}", location(reader));

        String localName;
        while (reader.hasNext()) {
            reader.next();

            if (reader.isStartElement()) {
                localName = reader.getLocalName();
                if ("td".equalsIgnoreCase(localName) || "th".equalsIgnoreCase(localName)) {
                    r.add(loadTableCell(reader, ""));
                } else {
                    skipFullElement(reader);
                }
            } else if (reader.isEndElement() && tagName.equalsIgnoreCase(reader.getLocalName())) {
                break;
            }
        }

        return r;
    }

    /**
     * 从Reader中读取表格单元格元素，如果该元素没有指定样式类名则使用默认的样式类名。
     *
     * @param reader
     *         读取XML的Reader。
     * @param defaultClassName
     *         默认的样式类名。
     * @return 读取的元素。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @see #loadTableRow(XMLStreamReader, String)
     */
    private static TableCell loadTableCell(
            XMLStreamReader reader,
            String defaultClassName
    )
            throws XMLStreamException {
        rejectIfNull(reader, "reader");

        final String className = loadClassName(reader);
        final String tagName = reader.getLocalName();

        final String colSpan = loadAttribute(reader, "colSpan");
        final String rowSpan = loadAttribute(reader, "rowSpan");
        final String width = loadAttribute(reader, "width");

        final TableCell c = new TableCell(className.isEmpty() ? defaultClassName : className, toInt(colSpan, 0), toInt(rowSpan, 0), toInt(width, 0));

        LOGGER.trace("[TABLE CELL] at {}", location(reader));

        String localName;
        while (reader.hasNext()) {
            reader.next();

            if (reader.isStartElement()) {
                localName = reader.getLocalName();
                if ("p".equalsIgnoreCase(localName)) {
                    c.add(loadParagraph(reader, "th".equals(tagName) ? "[TH]" : "[TD]"));
                } else {
                    skipFullElement(reader);
                }
            } else if (reader.isEndElement() && tagName.equalsIgnoreCase(reader.getLocalName())) {
                break;
            }
        }

        return c;
    }

    /**
     * 从Reader中读取纯文本元素。
     *
     * @param reader
     *         读取XML的Reader。
     * @param strong
     *         默认是否使用粗体。
     * @return 读取的元素。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @throws IllegalStateException
     *         如果Reader当前位置不存在文本元素。
     */
    private static Text loadText(
            XMLStreamReader reader,
            boolean strong
    ) {
        String className = "";
        if (strong) {
            className = "strong";
        }
        return new Text(className, reduceWhitespace(reader.getText()).replaceFirst("^\\s+", ""));
    }

    /**
     * 从Reader中读取span元素。
     *
     * @param reader
     *         读取XML的Reader。
     * @param strong
     *         默认是否使用粗体。
     * @return 读取的元素。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalStateException
     *         如果Reader当前位置不存在{@literal span}元素。
     */
    private static Text loadSpan(
            XMLStreamReader reader,
            boolean strong
    )
            throws XMLStreamException {
        String className = loadClassName(reader);
        if (strong)
            className = className + " strong";
        reader.next();
        return new Text(className, reduceWhitespace(reader.getText()));

    }

    /**
     * 从Reader中读取超链接元素。
     *
     * @param reader
     *         读取XML的Reader。
     * @param strong
     *         默认是否使用粗体。
     * @return 读取的元素。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @throws XMLStreamException
     *         如果读取XMl文件出错。
     * @throws IllegalStateException
     *         如果Reader当前位置的元素没有内部文本({@literal innerText})。
     */
    private static HyperLink loadHyperLink(
            XMLStreamReader reader,
            boolean strong
    )
            throws XMLStreamException {
        String className = loadClassName(reader);
        if (strong)
            className = className + " strong";
        final String href = loadAttribute(reader, "href");

        return new HyperLink(className, reduceWhitespace(reader.getElementText()), href);
    }

    private static Img loadImg(
            XMLStreamReader reader,
            String defaultClassName
    )
            throws XMLStreamException {
        String className = loadClassName(reader);
        final String src = loadAttribute(reader, "src");

        return new Img(className.isEmpty() ? defaultClassName : className, src);
    }

    private static FullImg loadFullImg(
            XMLStreamReader reader,
            String defaultClassName
    )
            throws XMLStreamException {
        String className = loadClassName(reader);
        final String src = loadAttribute(reader, "src");

        return new FullImg(className.isEmpty() ? defaultClassName : className, src);
    }

    /**
     * 从Reader中读取元素的样式类名。
     *
     * @param reader
     *         读取XML的Reader。
     * @return 样式类名，如果未指定则返回空字符串，自动去掉首尾空格。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @throws IllegalStateException
     *         如果Reader当前位置不是元素的开始标记，因为开始标记和自封闭的元素才可以读取属性。
     */
    private static String loadClassName(XMLStreamReader reader) {
        return loadAttribute(reader, "class").trim();
    }

    /**
     * 从Reader中读取元素的属性值。
     *
     * @param reader
     *         读取XML的Reader。
     * @param name
     *         属性名，自动去掉首尾空格。
     * @return 属性值，如果未指定则返回空字符串。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     * @throws IllegalStateException
     *         如果Reader当前位置不是元素的开始标记，因为开始标记和自封闭的元素才可以读取属性。
     */
    private static String loadAttribute(
            XMLStreamReader reader,
            String name
    ) {
        name = rejectIfBlank(name, "name").trim();

        final int n = reader.getAttributeCount();
        for (int i = 0; i < n; ++i) {
            final String localName = reader.getAttributeLocalName(i);
            if (name.equalsIgnoreCase(localName)) {
                return reader.getAttributeValue(i);
            }
        }
        return "";
    }

    /**
     * 使Reader 跳过一个完整的元素。
     *
     * @param reader
     *         读取XML的Reader。
     * @throws XMLStreamException
     *         如果读取XML文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     */
    private static void skipFullElement(
            XMLStreamReader reader
    )
            throws XMLStreamException {
        rejectIfNull(reader, "reader");

        final String s = reader.getLocalName();
        int d = 0;
        while (reader.hasNext()) {
            if (reader.isStartElement()) {
                ++d;
            } else if (reader.isEndElement()) {
                if (d == 0) {
                    if (!s.equalsIgnoreCase(reader.getLocalName())) {
                        LOGGER.error("mismatched close tag \"{}\" at {}", reader.getLocalName(), location(reader));
                    }
                    break;
                } else {
                    --d;
                }
            }
        }
    }

    /**
     * 将所有连续的空白字符替换为单独空格。
     *
     * @param s
     *         待替换的字符串。
     * @return 已替换的字符串，如果参数{@code s}是{@code null}则返回空字符串。
     */
    private static String reduceWhitespace(
            String s
    ) {
        return s != null ? s.replaceAll("\\s+", " ") : "";
    }

    /**
     * 获取Reader的当前位置信息。
     *
     * @param reader
     *         读取XML的Reader。
     * @return Reader的当前位置信息，包括行号，列号等等。
     * @throws XMLStreamException
     *         如果读取XML文件出错。
     * @throws IllegalArgumentException
     *         如果参数{@code reader}是{@code null}。
     */
    private static String location(
            XMLStreamReader reader
    )
            throws XMLStreamException {
        final Location loc = reader.getLocation();
        return "[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Document load(InputStream stream)
            throws IOException {
        return load(stream, "utf-8");
    }

    /**
     * 从HTML文档输入流中加载文档。
     *
     * @param stream
     *         包含HTML文档的输入流。
     * @param encoding
     *         HTML文档的编码格式。
     * @return 加载的文档。
     * @throws IOException
     *         从输入流中加载文档时出现错误。
     */
    public final Document load(
            InputStream stream,
            String encoding
    )
            throws IOException {
        final XMLInputFactory factory = XMLInputFactory.newFactory();

        factory.setProperty(XMLInputFactory.IS_COALESCING, true);
        factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true);
        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);

        XMLStreamReader reader = null;

        try {
            reader = factory.createXMLStreamReader(stream, encoding);

            // Skip to body.
            while (!(reader.isStartElement() && BODY_TAG_NAME.equalsIgnoreCase(reader
                                                                                       .getLocalName()))) {
                reader.next();
            }

            final Document ret = new Document();

            for (final Block b : loadBlocks(reader)) {
                if (b != null) {
                    ret.add(b);
                }
            }

            return ret;
        } catch (XMLStreamException | FactoryConfigurationError ex) {
            throw new IOException(ex);
        } finally {
            try {
                if (reader != null)
                    reader.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * @author Haart
     */
    private static class BlockIterator
            implements Iterator<Block> {
        private final XMLStreamReader reader;
        private BlockIterator innerIterator = null;

        public BlockIterator(
                XMLStreamReader reader
        ) {
            this.reader = rejectIfNull(reader, "reader");
        }

        @Override
        public boolean hasNext() {
            if (this.innerIterator != null) {
                if (this.innerIterator.hasNext()) {
                    return true;
                } else {
                    this.innerIterator = null;
                    return false;
                }
            }

            try {
                while (reader.hasNext()) {
                    reader.next();
                    if (reader.isStartElement()) {
                        return true;
                    }
                }
                return false;
            } catch (XMLStreamException ex) {
                LOGGER.error("failed to load blocks", ex);
                return false;
            }
        }

        @Override
        public Block next() {
            try {
                final String tagName = reader.getLocalName();
                if ("p".equalsIgnoreCase(tagName)) {
                    return loadParagraph(reader);
                } else if ("h1".equalsIgnoreCase(tagName)) {
                    return loadParagraph(reader, "[H1]");
                } else if ("h2".equalsIgnoreCase(tagName)) {
                    return loadParagraph(reader, "[H2]");
                } else if ("h3".equalsIgnoreCase(tagName)) {
                    return loadParagraph(reader, "[H3]");
                } else if ("h4".equalsIgnoreCase(tagName)) {
                    return loadParagraph(reader, "[H4]");
                } else if ("h5".equalsIgnoreCase(tagName)) {
                    return loadParagraph(reader, "[H5]");
                } else if ("table".equalsIgnoreCase(tagName)) {
                    return loadTable(reader);
                } else if ("img".equalsIgnoreCase(tagName)) {
                    return loadFullImg(reader, "[IMG]");
                } else if ("hr".equalsIgnoreCase(tagName)) {
                    return loadPageBreaker(reader);
                } else if ("div".equalsIgnoreCase(tagName)) {
                    this.innerIterator = new BlockIterator(reader);
                    return null;
                } else {
                    LOGGER.warn("invalid block element <{}> at {}", tagName, location(reader));
                    skipFullElement(reader);
                    return null;
                }
            } catch (XMLStreamException ex) {
                LOGGER.error("failed to load blocks", ex);
                return null;
            }
        }

        @Override
        public void remove() {
        }
    }
}
